<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue虚拟DOM渲染页面完整版</title>
</head>
<body>
  <div id="root">
    <div class="c1">
      <div title="tt1" id="id">{{ name }}</div>
      <div title="tt2">{{ age }}</div>
      <div title="tt3">{{ gender }}</div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </div>
  </div>
  
  <script>
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    /** 虚拟 DOM 构造函数 */
    class VNode {
      constructor (tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = []
      }
      appendChild(vnode) {
        this.children.push(vnode)
      }
    }
    /** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
    function getVNode( node ) {
      const nodeType = node.nodeType
      let _node = null
      if (nodeType === 1) { // 元素节点
        const nodeName = node.nodeName // 获取元素标签
        const attrs = node.attributes // 获取元素属性 伪数组
        const objAttrs = {}
        // 循环
        for (let i = 0;  i < attrs.length; i++) {
          objAttrs[attrs[i].nodeName] = attrs[i].nodeValue
        }
        _node = new VNode(nodeName, objAttrs, undefined, nodeType)
        // 节点下面还有节点
        const childNode = node.childNodes
        for ( let i = 0; i < childNode.length; i++ ) {
          _node.appendChild( getVNode(childNode[i]) ) // 递归
        }
      } else if (nodeType === 3){ // 文本节点
        _node = new VNode(undefined, undefined, node.textContent, nodeType)
      }
      return _node
    }
    
    function getValueByPath(data, value) {
      let arr = value.split('.')
      let res = data
      let prop
      while( prop = arr.shift() ) {
        res = res[prop]
      }
      return res
    }
    /** 将带有坑的Vnode与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
    function compile (vnode, _data) {
      const data = vnode.data
      let value = vnode.value
      const type = vnode.type
      const children = vnode.children
      const tag = vnode.tag

      let _vnode = null
      if (type === 3) { // 文本节点
        value = value.replace(regMustache, function (node, val) {
          return getValueByPath( _data, val)
        })
        _vnode = new VNode( tag, data, value, type )
      } else if (type === 1) { // 元素节点
        _vnode = new VNode(tag, data, value, type)
        children.forEach(child => {
          _vnode.appendChild(compile(child, _data))
        })
      }
      return _vnode
    }
     // 将 vNode 转换为真正的 DOM
    function parseVNode( vnode ) {
      const nodeType = vnode.type
      let _node = null;
      if (nodeType === 3) { // 是文本
        return document.createTextNode(vnode.value)
      } else if (nodeType === 1) {// 是节点
        _node = document.createElement(vnode.tag)
        // 获取节点属性
        const data = vnode.data
        Object.keys(data).forEach(key => {
          const attrValue = data[key]
          _node.setAttribute(key , attrValue)
        })
        const children = vnode.children
        children.forEach(subvnode => {
          _node.appendChild( parseVNode( subvnode ) )
        })
        return _node
      }
    }
    
    /** 解析DOM，由真实DOM转变成VDOM，再插入数据到VNode里面 */
    class Vue {
      constructor($options) {
        this._data = $options.data
        this._el = document.querySelector($options.el)
        this._parent = this._el.parentNode
        // 把数据做成响应式
        reactify(this._data, this)
        this.mount()
      }
      mount () {
        // 提供一个 render 方法: 生成 虚拟 DOM
        this.render = this.createRenderFn()
        this.mountComponent()
      }
      mountComponent() {
        let mount = () => {
          this.update(this.render())
        }
        mount.call(this) // 交给 watcher 来调用
      }
      createRenderFn () {
        return function render () {
          let ast = getVNode(this._el)
          let temp = compile(ast, this._data)
          return temp
        }
      }
      // 将虚拟 DOM 渲染到页面中: diff 算法就在里
      update(vnode) {
        // 获取返回真实DOM节点树
        let realDom = parseVNode(vnode)
        // 替换模板渲染到页面上
        this._parent.replaceChild(realDom, document.querySelector('#root'))
        // 这个算法是不负责任的: 
        // 每次会将页面中的 DOM 全部替换
      }
    }
    // 数组响应式
    function ArrayFunction () {
      const ARRAY_METHOD = ['push','pop','shift','unshift','reverse','sort','splice']
      const array_method = Object.create(Array.prototype)
      ARRAY_METHOD.forEach(method => {
        array_method[method] = function () {
          // 将数据进行响应式化
          Array.from(arguments).forEach(item => {
            reactify(item)
          })
          // 把当前方法绑定到Array.prototype的原型上
          const res = Array.prototype[method].apply(this, arguments)
          return res
        }
      })
      return array_method
    }
    // 使数据响应化
    function defineReactive (target, key, value, enumerable) {
      const that = this
      Object.defineProperty(target, key, {
        enumerable: !!enumerable,
        configurable: true,
        get () {
          return value
        },
        set (newValue) {
          if (typeof newValue === 'object' && newValue !== null) {
            value = reactify(newValue)
          } else {
            value = newValue
          }
          that.mountComponent()
        }
      })
    }
    // 对象响应化
    function reactify (data, vm) {
      Object.keys(data).forEach(key => {
        let value = data[key]
        if (Array.isArray(value)) {
          value.__proto__ = ArrayFunction()
          value.forEach(item => {
            reactify(item, vm) // 递归
          })
        } else if (typeof value === 'object' && value !== null && !Array.isArray( value )) {
          reactify(value)
        } else {
          defineReactive.call(vm, data, key, value, true)
        }
      })
    }
    const app = new Vue({
      el: '#root',
      data: {
        name: '张三',
        age:'19',
        gender: '男',
        course: [
          {
            name: '语文'
          },
          {
            name: '数学'
          },
          {
            name: '英语'
          }
        ]
      }
    })
  </script>
</body>
</html>